package cn.zhejianglao.shirotemplate.config.mybatis;

import cn.zhejianglao.shirotemplate.common.model.PageRequest;
import cn.zhejianglao.shirotemplate.exception.BusinessException;
import cn.zhejianglao.shirotemplate.exception.ExceptionCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;

import java.lang.reflect.InvocationTargetException;
import java.sql.*;
import java.util.Map;
import java.util.Properties;

/**
 * 分页查询拦截器
 */
@Intercepts({@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class})
})
@Slf4j
public class PagedSelectInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = getActualHandlerObject(invocation);
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        String sql = statementHandler.getBoundSql().getSql();
        // 检测未通过，不是select语句
        if (!checkIsSelectFlag(sql)) {
            return invocation.proceed();
        }
        BoundSql boundSql = statementHandler.getBoundSql();
        Object paramObject = boundSql.getParameterObject();
        PageRequest pageParam = getPageParam(paramObject);
        if (pageParam == null) {
            return invocation.proceed();
        }
        int pageNum = pageParam.getPage();
        int pageSize = pageParam.getSize();
        int total = getTotal(invocation, metaStatementHandler, boundSql);
        // 将动态获取到的分页参数回填到pageParam中
        setTotalToParam(pageParam, total, pageSize);
        // 检查当前页码的有效性
        checkPage(pageNum, pageParam.getTotalPage());
        // 修改sql
        return updateSql2Limit(invocation, metaStatementHandler, pageNum, pageSize);
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
    }

    // 从代理对象中分离出真实statementHandler对象,非代理对象
    private StatementHandler getActualHandlerObject(Invocation invocation) {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        Object object = null;
        // 分离代理对象链，目标可能被多个拦截器拦截，分离出最原始的目标类
        while (metaStatementHandler.hasGetter("h")) {
            object = metaStatementHandler.getValue("h");
            metaStatementHandler = SystemMetaObject.forObject(object);
        }
        if (object == null) {
            return statementHandler;
        }
        return (StatementHandler) object;
    }

    // 判断是否是select语句，只有select语句，才会用到分页
    private boolean checkIsSelectFlag(String sql) {
        String trimSql = sql.trim();
        int index = trimSql.toLowerCase().indexOf("select");
        return index == 0;
    }

    /**
     * 获取分页的参数
     * 参数可以通过map，@param注解进行参数传递。或者请求pojo继承自PageParam，将PageParam中的分页数据放进去
     */
    private PageRequest getPageParam(Object paramerObject) {
        if (paramerObject == null) {
            return null;
        }
        PageRequest pageParam = null;
        // 通过map和@param注解将PageParam参数传递进来，pojo继承自PageParam不推荐使用  这里从参数中提取出传递进来的pojo继承自PageParam
        // 首先处理传递进来的是map对象和通过注解方式传值的情况，从中提取出PageParam,循环获取map中的键值对，取出PageParam对象
        if (paramerObject instanceof Map) {
            Map<String, Object> params = (Map<String, Object>) paramerObject;
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                if (entry.getValue() instanceof PageRequest) {
                    return (PageRequest) entry.getValue();
                }
            }
        } else if (paramerObject instanceof PageRequest) {
            // 继承方式 pojo继承自PageParam 只取出我们希望得到的分页参数
            pageParam = (PageRequest) paramerObject;
        }
        return pageParam;
    }

    // 获取当前sql查询的记录总数
    private int getTotal(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql) {
        // 获取mapper文件中当前查询语句的配置信息
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler
                .getValue("delegate.mappedStatement");
        // 获取所有配置Configuration
        org.apache.ibatis.session.Configuration configuration = mappedStatement.getConfiguration();
        // 获取当前查询语句的sql
        String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
        // 删除结尾的 ;
        sql = sql.replaceAll(";$", "");
        // 将sql改写成统计记录数的sql语句,这里是mysql的改写语句,将第一次查询结果作为第二次查询的表
        String countSql = "select count(*) as total from (" + sql + ") $_paging";
        // 获取connection连接对象，用于执行countsql语句
        Connection conn = (Connection) invocation.getArgs()[0];
        PreparedStatement ps = null;
        int total = 0;
        try {
            // 预编译统计总记录数的sql
            ps = conn.prepareStatement(countSql);
            // 构建统计总记录数的BoundSql
            BoundSql countBoundSql = new BoundSql(
                    configuration, countSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
            // 构建ParameterHandler，用于设置统计sql的参数
            ParameterHandler parameterHandler = new DefaultParameterHandler(
                    mappedStatement, boundSql.getParameterObject(), countBoundSql);
            // 设置总数sql的参数
            parameterHandler.setParameters(ps);
            // 执行查询语句
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                // 与countSql中设置的别名对应
                total = rs.getInt("total");
            }
        } catch (SQLException e) {
            throw new BusinessException(
                    ExceptionCode.Runtime.SQL_EXCEPTION_CODE,
                    ExceptionCode.Runtime.SQL_EXCEPTION_MSG
            );
        } finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    log.error("关闭sql连接异常");
                }
        }
        return total;
    }

    // 设置条数参数到 page param 对象
    private void setTotalToParam(PageRequest param, int total, int pageSize) {
        param.setTotal(total);
        param.setTotalPage(total % pageSize == 0 ? total / pageSize : (total / pageSize) + 1);
    }

    // 验证当前页码的有效性
    private void checkPage(Integer pageNumber, Integer pageTotal) throws Exception {
        if (pageTotal > 0 && pageNumber > pageTotal) {
            throw new Exception("查询失败，查询页码" + pageNumber + "大于总页数" + pageTotal);
        }
    }

    // 修改原始sql语句为分页sql语句
    private Object updateSql2Limit(Invocation invocation, MetaObject metaStatementHandler, int page, int pageSize)
            throws IllegalAccessException, SQLException, InvocationTargetException {
        String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
        // 删除结尾的 ;
        sql = sql.replaceAll(";$", "");
        // 构建新的分页sql语句
        String limitSql = "select * from (" + sql + ") $_paging_table limit ?,?";
        // 修改当前要执行的sql语句
        metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);
        // 相当于调用prepare方法，预编译sql并且加入参数，但是少了分页的两个参数，它返回一个PreparedStatement对象
        PreparedStatement ps = (PreparedStatement) invocation.proceed();
        // 获取sql总的参数总数
        int count = ps.getParameterMetaData().getParameterCount();
        // 设置与分页相关的两个参数
        ps.setInt(count - 1, (page - 1) * pageSize);
        ps.setInt(count, pageSize);
        return ps;
    }
}
